package com.lemon.exam.common.util;

import cn.idev.excel.FastExcel;
import com.github.benmanes.caffeine.cache.AsyncCache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.lemon.exam.common.constant.ApiConst;
import com.lemon.exam.common.enums.QuestionTypeEnum;
import com.lemon.exam.common.exception.CustomException;
import com.lemon.exam.common.listener.PaperReadListener;
import com.lemon.exam.entity.dto.PaperExcelDTO;
import com.lemon.exam.entity.vo.PaperQuestionVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

import java.io.*;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 试卷工具类
 *
 * @author Lemon
 * @since 2025/7/22 14:43
 */
@Slf4j
public class PaperUtil {
    /**
     * 试卷试题 异步缓存
     * key:试题path
     * value:试题
     */
    private static final AsyncCache<String, List<PaperExcelDTO>> asyncPaperCache = Caffeine.newBuilder()
            .expireAfterWrite(30, TimeUnit.MINUTES)
            .maximumSize(10000)
            .recordStats()
            .buildAsync();

    /**
     * 获取试卷中的试题，先从异步缓存中获取，如果没有则从Excel文件中获取
     *
     * @param path
     * @return
     */
    public static Mono<List<PaperExcelDTO>> getPaperExcel(String path) {
        return Mono.fromFuture(asyncPaperCache.get(path, PaperUtil::loadQuestionData)).subscribeOn(Schedulers.boundedElastic());
    }

    /**
     * 获取试卷中的试题
     *
     * @param path
     * @return
     */
    public static Mono<List<PaperQuestionVO>> getPaperQuestion(String path) {
        return getPaperExcel(path).flatMap(excel -> Mono.fromCallable(() -> excel.stream()
                        .map(DTO_TO_VO)
                        .toList())
                .subscribeOn(Schedulers.boundedElastic()));
    }

    /**
     * 获取试卷中的试题
     *
     * @param path
     * @param isUnshift 是否打乱顺序
     * @return
     */
    public static Mono<List<PaperQuestionVO>> getPaperQuestion(String path, boolean isUnshift) {
        if (!isUnshift) {
            return getPaperQuestion(path);
        }

        /**
         * 1.按题型分组并保持插入顺序
         * 2.打乱每组内的题目顺序
         * 3.合并回一个列表
         */
        return getPaperQuestion(path)
                .map(questions -> questions.stream()
                        .collect(Collectors.groupingBy(
                                PaperQuestionVO::getType,
                                LinkedHashMap::new,
                                Collectors.toList()
                        ))
                        .values()
                        .stream()
                        .peek(Collections::shuffle)
                        .flatMap(List::stream)
                        .toList())
                .subscribeOn(Schedulers.boundedElastic());
    }


    /**
     * 从Excel中获取试卷中的试题
     *
     * @param path
     * @return
     */
    private static List<PaperExcelDTO> loadQuestionData(String path) {
        if (!StringUtils.hasText(path)) {
            throw new CustomException(404, "文件路径为空！");
        }
        log.info("开始加载试题数据：{}", path);

        //1.根据文件路径获取文件
        File file = new File(path);

        //2.检查文件是否存在
        if (!file.exists() || file.length() == 0) {
            throw new CustomException(404, "试题文件不存在");
        }

        //3.使用缓冲流读取
        try (InputStream is = new BufferedInputStream(new FileInputStream(file))) {
            PaperReadListener listener = new PaperReadListener();
            FastExcel.read(is, PaperExcelDTO.class, listener).sheet().headRowNumber(2).doRead();
            return listener.getQuestions().isEmpty() ? Collections.emptyList() : listener.getQuestions();
        } catch (Exception e) {
            throw new CustomException("加载试题数据失败:%s".formatted(e.getMessage()));
        }
    }

    /**
     * 将DTO转换为VO
     */
    private static final Function<PaperExcelDTO, PaperQuestionVO> DTO_TO_VO = dto -> {
        PaperQuestionVO vo = new PaperQuestionVO();
        vo.setRowIndex(dto.getRowIndex());
        vo.setType(dto.getType());
        vo.setQuestion(dto.getQuestion());
        vo.setAnswer(StringUtils.hasText(dto.getAnswer()) ? dto.getAnswer().split("\\|") : ApiConst.EMPTY_STRING_ARRAY);
        vo.setAnalysis(dto.getAnalysis());
        vo.setTag(StringUtils.hasText(dto.getTag()) ? dto.getTag().split("\\|") : null);
        vo.setScore(StringUtils.hasText(dto.getScore()) ? Double.parseDouble(dto.getScore()) : null);

        Map<String, String> options = switch (QuestionTypeEnum.fromType(dto.getType())) {
            case JUDGMENT -> Map.of("A", "正确", "B", "错误");
            case SINGLE_CHOICE, MULTIPLE_CHOICE -> Stream.of(
                            Map.entry("A", StringUtils.hasText(dto.getOptionA()) ? dto.getOptionA().trim() : ""),
                            Map.entry("B", StringUtils.hasText(dto.getOptionB()) ? dto.getOptionB().trim() : ""),
                            Map.entry("C", StringUtils.hasText(dto.getOptionC()) ? dto.getOptionC().trim() : ""),
                            Map.entry("D", StringUtils.hasText(dto.getOptionD()) ? dto.getOptionD().trim() : ""),
                            Map.entry("E", StringUtils.hasText(dto.getOptionE()) ? dto.getOptionE().trim() : ""),
                            Map.entry("F", StringUtils.hasText(dto.getOptionF()) ? dto.getOptionF().trim() : ""),
                            Map.entry("G", StringUtils.hasText(dto.getOptionG()) ? dto.getOptionG().trim() : ""),
                            Map.entry("H", StringUtils.hasText(dto.getOptionH()) ? dto.getOptionH().trim() : ""),
                            Map.entry("I", StringUtils.hasText(dto.getOptionI()) ? dto.getOptionI().trim() : ""),
                            Map.entry("J", StringUtils.hasText(dto.getOptionJ()) ? dto.getOptionJ().trim() : "")
                    )
                    .filter(entry -> StringUtils.hasText(entry.getValue()))
                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
            default -> Collections.emptyMap();
        };

        //设置选项
        vo.setOptions(options);
        return vo;
    };

    /**
     * 解析Excel文件并获取指定列的数据（会过滤掉空数据）
     *
     * @param filePart    文件
     * @param startRow    开始行 开始值为0
     * @param columnIndex 列索引 开始值为0
     * @return
     */
    public static Flux<String> parseExcelAndGetColumnData(FilePart filePart, int startRow, int columnIndex) {
        return DataBufferUtils.join(filePart.content()).flatMapMany(dataBuffer -> {
            try (InputStream inputStream = dataBuffer.asInputStream()) {
                Workbook workbook = WorkbookFactory.create(inputStream);
                Sheet sheet = workbook.getSheetAt(0);

                return Flux.range(startRow, Math.max(0, sheet.getLastRowNum() - startRow + 1))
                        .map(sheet::getRow)
                        .filter(Objects::nonNull) // 过滤空行
                        .map(row -> row.getCell(columnIndex, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK))
                        .filter(cell -> null != cell && cell.getCellType() != CellType.BLANK)
                        .map(cell -> cell.getStringCellValue().trim())
                        .filter(StringUtils::hasText) // 过滤空字符串
                        .doFinally(signal -> {
                            try {
                                workbook.close();
                            } catch (IOException e) {
                                log.error("关闭Workbook失败", e);
                            }
                            DataBufferUtils.release(dataBuffer);
                        });
            } catch (Exception e) {
                DataBufferUtils.release(dataBuffer);
                return Flux.error(new CustomException(500, "解析Excel文件失败：%s".formatted(e.getMessage())));
            }
        });
    }
}
